上一篇已經讓編輯器可以顯示畫面並讀取方向鍵。這一篇將開啟實際的文字檔,把內容載入記憶體,再用 viewport 決定終端機目前顯示哪幾行。完成後,程式可以開啟文字檔。游標移到畫面底端後繼續按 ↓,將內容向下捲動。
程式開啟檔案時,會把檔案內容載入記憶體。editor 用 rows 保存每一行文字,filename 保存檔案名稱,供狀態列顯示:
type editor struct {
rows []string // 每個元素保存一行文字。
filename string
}
rows[0] 是檔案第 1 行,rows[120] 是第 121 行。使用行陣列後,編輯器可以直接用行號取得游標所在行或畫面要顯示的幾行。
openFile 使用 bufio.Scanner 逐行讀取檔案:
func (e *editor) openFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
e.filename = name
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
// Scanner 每次回傳一行,內容不包含換行符。
e.rows = append(e.rows, sc.Text())
}
return sc.Err()
}
當檔案的行數超過畫面高度時,終端機一次只能顯示其中一部分。例如用 24 行高的畫面瀏覽 500 行檔案時,rowOffset 會記錄畫面第一行對應到檔案哪一行。
假設 rowOffset == 100:
螢幕第 1 行顯示檔案第 101 行
螢幕第 2 行顯示檔案第 102 行
程式從 0 開始計算位置,因此螢幕行 screenY 對應的檔案行是:
fileRow = rowOffset + screenY
cy 記錄游標位於檔案第幾行。當游標移動超出目前畫面範圍時,scroll 會更新 rowOffset 調整 Viewport:
func (e *editor) scroll() {
// 游標移到目前畫面上方時,讓該檔案行成為畫面第一行。
if e.cy < e.rowOffset {
e.rowOffset = e.cy
}
// 游標移到目前畫面下方時,讓該檔案行成為畫面最後一行。
if e.cy >= e.rowOffset+e.screenRows {
e.rowOffset = e.cy - e.screenRows + 1
}
}
drawRows 使用 rowOffset 將螢幕行號換算成檔案行號,決定每個螢幕行繪製哪一行內容:
func (e *editor) drawRows(b *bytes.Buffer) {
// 依序處理終端機中可用的每一行。
for screenY := 0; screenY < e.screenRows; screenY++ {
// 把螢幕行號換算成檔案行號。
fileRow := e.rowOffset + screenY
if fileRow >= len(e.rows) {
// 超出檔案結尾的畫面行顯示 ~。
b.WriteString("~")
} else {
// 將對應的檔案行加入畫面 buffer。
b.WriteString(e.rows[fileRow])
}
// 清除舊內容的行尾,再移到下一個螢幕行。
b.WriteString("\x1b[K\r\n")
}
}
每次按鍵處理完後,主迴圈會呼叫 refreshScreen。它先透過 scroll 更新 Viewport 偏移量,接著讓 drawRows 繪製對應行,最後將檔案游標座標換算為終端機螢幕座標:
func (e *editor) refreshScreen() {
e.scroll()
var b bytes.Buffer
b.WriteString("\x1b[?25l\x1b[H") // 隱藏游標並移回左上角
e.drawRows(&b)
e.drawStatusBar(&b)
// 把檔案座標 (cx, cy) 扣除 Viewport 偏移量,換算成終端機螢幕座標
fmt.Fprintf(&b, "\x1b[%d;%dH", e.cy-e.rowOffset+1, e.cx-e.colOffset+1)
b.WriteString("\x1b[?25h")
os.Stdout.Write(b.Bytes())
}
在 cli-sample/go-editor-tutorial/ 執行:
go run ./open-file-viewport testdata/lines.txt
程式啟動後會進入全螢幕,畫面顯示 testdata/lines.txt 的前幾行內容,底部的反白狀態列顯示檔名、總行數與目前游標位置:
line 001: opening a file
line 002: reading one line at a time
...
line 023: one more line
testdata/lines.txt - 201 lines 1/201
持續按下 ↓ 將游標移至畫面最底端後,再按一次 ↓,scroll 會增加 rowOffset,畫面隨即向下捲動顯示 line 024,狀態列游標行數同步更新為 24/201。按 Ctrl+Q 即可結束程式。
下一篇會在 rows 上加入文字修改,並把修改後的內容存回磁碟。